Yelp Dataset- Stars Prediction
Aprendizaje Automático
Master Universitario en Ciencia de Datos
Antonio Nogués Podadera (antonio.nogues@cunef.edu)
Víctor Viloria Vázquez (victor.viloria@cunef.edu)
1. Librerias utlizadas y entorno
Esta práctica consiste en la exploración e investigación de casos de negocio aplicables al dataset yelp_academic_dataset. Para ello deberemos aplicar todo lo aprendido durante el curso, además de trabajar con visualizaciones geográficas, gráfos y procesamiento de archivos de gran capacidad y de tipo JSON (Previamente realizado en 00_Preprocessing).
En este notebook unicamente se explicará el objetivo de negocio y se realizará un análisis descriptivo de los datos.
El objetivo de negocio es a través de la información registrada por los negocios en la plataforma y sus características, poder predecir si el promedio de puntuación que alcanzarán por parte de los usuarios será alto o bajo.
Para ello trabajaremos con 2 ficheros: business_df y categories_df además nos centraremos principalmente en los locales con categoría 'Restaurante'.
Nuestro objetivo se caracteriza por ser un problema de clasificación supervisado.
Improtamos las librerias a utilizar:
# Ignore warnings
import warnings
warnings.filterwarnings("ignore")
# General.
import pandas as pd
import numpy as np
# Visualization.
import matplotlib.pyplot as plt
import plotly.express as px
import seaborn as sns
import folium
# Train, Validation, Test split.
from sklearn.model_selection import train_test_split
Importamos las funciones que vamos a utilizar durante el EDA.
# Importing the functions:
from aux_functions import *
from transformations import count_checkins
En este punto se procederá a cargar los datos previamente procesados en 0_DataPreprocessing, se unificarán los datos que sean interesantes para nuestro caso de negocio y se analizarán los formatos de las variables.
Lectura del archivo business_df y categories_df generados en 0_DataPreprocessing. Nos serán necesarios para filtrar la variable business_id por categorías relacionadas con la hostelería y obtener sus detalles/ atributos.
# Load the data business_df.parquet
business_df = pd.read_parquet('../data/processed/business_df.parquet')
# Load the data review_df.parquet
categories_df = pd.read_parquet('../data/processed/categories_df.parquet')
En checkin_df disponemos de las fechas en las que los usuarios han publicado si han estado en el local, puede que nos resulte útil, por lo que cargaremos el archivo y extraeremos el total de visitas en función de la variable business_id.
# Load the data checkin_df.parquet.
checkin_df = pd.read_parquet('../data/raw/yelp_academic_dataset_checkin.parquet')
# Count of checkins per business_id.
checkin_df['total_checkins'] = checkin_df['date'].apply(count_checkins)
Procedemos a analizar la estructura inicial de los dataframes business_df, categories_df y checkin_df.
# Verification of the shape for 'business_df'
print("business_df contiene " + str(business_df.shape[0]) +" filas y " + str(business_df.shape[1]) + " columnas")
# Verification of the shape for 'categories_df'
print("categories_df contiene " + str(categories_df.shape[0]) +" filas y " + str(categories_df.shape[1]) + " columnas")
# Verification of the shape for 'checkin_df'
print("checkin_df contiene " + str(checkin_df.shape[0]) +" filas y " + str(checkin_df.shape[1]) + " columnas")
business_df contiene 150346 filas y 99 columnas categories_df contiene 150346 filas y 1313 columnas checkin_df contiene 131930 filas y 3 columnas
En este apartado se procederá a fusionar los datos que resulten útiles para el caso de negocio explicado anteriormente. En este caso, obtener una predicción de si el negocio va a tener una puntuación alta o no.
Se procede a incluir en business_df la variable total_checkins. Dado que puede resultar interesante saber el nº de visitas publicadas en la platadorma para calcular la predicción.
# Merge the checkin_df with the business_df by business_id.
business_df = pd.merge(business_df, checkin_df[['business_id', 'total_checkins']], on='business_id', how='inner')
# Display the first 5 rows of the business_df
business_df.head()
| business_id | name | address | city | state | postal_code | latitude | longitude | stars | review_count | ... | attributes_DietaryRestrictions_soy-free | attributes_DietaryRestrictions_vegetarian | total_hours_Monday | total_hours_Tuesday | total_hours_Wednesday | total_hours_Thursday | total_hours_Friday | total_hours_Saturday | total_hours_Sunday | total_checkins | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Pns2l4eNsfO8kk83dixA6A | Abby Rappoport, LAC, CMQ | 1616 Chapala St, Ste 2 | Santa Barbara | CA | 93101 | 34.4266787 | -119.7111968 | 5.0 | 7 | ... | nan | nan | nan | nan | nan | nan | nan | nan | nan | 1 |
| 1 | mpf3x-BjTdTEA3yCZrAYPw | The UPS Store | 87 Grasso Plaza Shopping Center | Affton | MO | 63123 | 38.551126 | -90.335695 | 3.0 | 15 | ... | nan | nan | 0.0 | 10.5 | 10.5 | 10.5 | 10.5 | 6.0 | nan | 15 |
| 2 | tUFrWirKiKi_TAnsVWINQQ | Target | 5255 E Broadway Blvd | Tucson | AZ | 85711 | 32.223236 | -110.880452 | 3.5 | 22 | ... | nan | nan | 14.0 | 14.0 | 14.0 | 14.0 | 15.0 | 15.0 | 14.0 | 401 |
| 3 | MTSW4McQd7CbVtyjqoe9mw | St Honore Pastries | 935 Race St | Philadelphia | PA | 19107 | 39.9555052 | -75.1555641 | 4.0 | 80 | ... | nan | nan | 13.0 | 13.0 | 13.0 | 13.0 | 14.0 | 14.0 | 14.0 | 335 |
| 4 | mWMc6_wTdE0EUBKIGXDVfA | Perkiomen Valley Brewery | 101 Walnut St | Green Lane | PA | 18054 | 40.3381827 | -75.4716585 | 4.5 | 13 | ... | nan | nan | nan | nan | 8.0 | 6.0 | 10.0 | 10.0 | 6.0 | 14 |
5 rows × 100 columns
Una vez definido el problema procederemos a quedarnos con los negocios que estén incluidos en la categoría Restaurants y que sigan abiertos. Una vez realizado el filtrado deshecharemos aquellas variables irrelevantes para el analisis. Para ello previamente deberemos realizar alguna trasnformación de formato de variables
Se puede observar que las variables referentes al número de horas que está abierto el establecimiento por días de la semana (total_hours_*) son de tipo objeto por lo que se procederá a transformarlas a tipo numérico.
El número de reviews que tiene el establecimiento (review_count) , total_checkins y Restaurants serán transformadas a tipo numérico también.
# Convert the type of data for each variable from 'object' to 'float64'.
business_df['total_hours_Monday'] = business_df['total_hours_Monday'].astype('float64')
business_df['total_hours_Tuesday'] = business_df['total_hours_Tuesday'].astype('float64')
business_df['total_hours_Wednesday'] = business_df['total_hours_Wednesday'].astype('float64')
business_df['total_hours_Thursday'] = business_df['total_hours_Thursday'].astype('float64')
business_df['total_hours_Friday'] = business_df['total_hours_Friday'].astype('float64')
business_df['total_hours_Saturday'] = business_df['total_hours_Saturday'].astype('float64')
business_df['total_hours_Sunday'] = business_df['total_hours_Sunday'].astype('float64')
# Convert 'review_count' and 'total_checkins' from 'object' to 'int64'.
business_df['review_count'] = business_df['review_count'].astype('int64')
business_df['total_checkins'] = business_df['total_checkins'].astype('int64')
business_df['is_open'] = business_df['is_open'].astype('int64')
# From the 'categories_df' dataframe, convert the 'Restaurants' variable from 'object' to 'int64'.
categories_df['Restaurants'] = categories_df['Restaurants'].astype('int64')
Procedemos a unir los dataframes en restaurants_df.
# Extract the business_id of the Restaurants in the 'categories_df' dataframe.
ids = categories_df[categories_df['Restaurants'] == 1]['business_id'].tolist()
# Join the variables of business_df with the 'ids' list.
restaurants_df = business_df[business_df['business_id'].isin(ids)].reset_index(drop=True)
# Drop the rows with 'is_open' = 0.
restaurants_df = restaurants_df[restaurants_df['is_open'] == 1].reset_index(drop=True)
# Convert the type of data for each variable since variable 8 to from 'object' to 'int64'. If an error occurs, skip to the next variable.
for i in range(6, len(restaurants_df.columns)):
try:
restaurants_df.iloc[:,i] = restaurants_df.iloc[:,i].astype('float64')
except:
continue
Mostramos como nos queda el dataframe a analizar: restaurants_df
# Display the first 5 rows of the restaurants_df.
restaurants_df.head()
| business_id | name | address | city | state | postal_code | latitude | longitude | stars | review_count | ... | attributes_DietaryRestrictions_soy-free | attributes_DietaryRestrictions_vegetarian | total_hours_Monday | total_hours_Tuesday | total_hours_Wednesday | total_hours_Thursday | total_hours_Friday | total_hours_Saturday | total_hours_Sunday | total_checkins | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | MTSW4McQd7CbVtyjqoe9mw | St Honore Pastries | 935 Race St | Philadelphia | PA | 19107 | 39.955505 | -75.155564 | 4.0 | 80.0 | ... | NaN | NaN | 13.0 | 13.0 | 13.0 | 13.0 | 14.0 | 14.0 | 14.0 | 335.0 |
| 1 | CF33F8-E6oudUQ46HnavjQ | Sonic Drive-In | 615 S Main St | Ashland City | TN | 37015 | 36.269593 | -87.058943 | 2.0 | 6.0 | ... | NaN | NaN | 0.0 | 16.0 | 16.0 | 16.0 | 15.0 | 13.0 | 14.0 | 22.0 |
| 2 | bBDDEgkFA1Otx9Lfe7BZUQ | Sonic Drive-In | 2312 Dickerson Pike | Nashville | TN | 37207 | 36.208102 | -86.768170 | 1.5 | 10.0 | ... | NaN | NaN | 0.0 | 15.0 | 15.0 | 10.0 | 10.0 | 11.0 | 15.0 | 21.0 |
| 3 | eEOYSgkmpB90uNA7lDOMRA | Vietnamese Food Truck | Tampa Bay | FL | 33602 | 27.955269 | -82.456320 | 4.0 | 10.0 | ... | NaN | NaN | 3.0 | 3.0 | 3.0 | 3.0 | 3.0 | 5.0 | 3.0 | 4.0 | |
| 4 | il_Ro8jwPlHresjw9EGmBg | Denny's | 8901 US 31 S | Indianapolis | IN | 46227 | 39.637133 | -86.127217 | 2.5 | 28.0 | ... | NaN | NaN | 16.0 | 16.0 | 16.0 | 16.0 | 16.0 | 16.0 | 16.0 | 54.0 |
5 rows × 100 columns
En este apartado procederemos a analizar el tipo de dato inicial que contiene cada variable.
# Verification of the different types of data in the 'restaurants_df'.
print(restaurants_df.dtypes.sort_values().to_frame('feature_type').groupby(by = 'feature_type').size().to_frame('count').reset_index())
# Print the type of data for each variable.
restaurants_df.dtypes
feature_type count 0 float64 87 1 object 13
business_id object
name object
address object
city object
state object
...
total_hours_Thursday float64
total_hours_Friday float64
total_hours_Saturday float64
total_hours_Sunday float64
total_checkins float64
Length: 100, dtype: object
Dado que durante el preprocesado guardamos los ficheros a tipo parquet, gran parte de ellos pasaron a ser de tipo object. Por lo que anteriormente realizamos la conversión numérica a la selección de variables con el objetivo de que puedan ser analizadas fácilmente.
Para un mayor entendimiento de los datos se ha elaborado un diccionario de datos. En él encontramos información detallada de cada una de las variables: Nombre, descripción, tipo de variable, tipo de dato y valores. Al ser los archivos originales de tipo JSON tuvieron que pasar por un preprocesado, de manera que nuestro dataset restaurants_df no tiene nada que ver con el original, por lo que hemos generado un nuevo diccionario procesado.
En cuanto al contenido de los datos, disponemos de una selección de transacciones realizadas y nos encontramos con 100 variables que proveen información acerca de los diferentes restaurantes, como localización, atributos, visitas, valoraciones...
Nos encontramos con dos tipos de variables:
Dictionary_processed.md.Analizamos el nº de valores distintos que adquiere cada variable, para tenerlo en cuenta en el diccionario.
uniqueValues = restaurants_df.nunique()
print('Número de valores distintos en cada variable :')
print(uniqueValues)
Número de valores distintos en cada variable :
business_id 34516
name 23102
address 32340
city 839
state 16
...
total_hours_Thursday 90
total_hours_Friday 89
total_hours_Saturday 96
total_hours_Sunday 88
total_checkins 1871
Length: 100, dtype: int64
La variable objetivo de este estudio es predecir si un restaurante va adquirir una puntuacion alta (4-5 estrellas) en función de sus atributos e información registrada en la plataforma Yelp. Para ello deberemos convertir la variable stars en binaria, agrupando las puntuaciones 4,5 en 1 y menores de 4 en 0. Observamos la distribución original:
# Graphic of the distribution of the variable 'stars'.
sns.set(style="whitegrid")
ax = sns.countplot(x="stars", data=restaurants_df, palette="Blues_d")
plt.title('Distribución de las estrellas')
plt.rcParams['figure.figsize'] = (20, 5)
plt.show()
Realizamos la conversión de los valores y representamos la nueva gráfica.
# Replace the stars values as a binary problem:
restaurants_df['stars'] = restaurants_df['stars'].apply(lambda x: 1 if x >= 4 else 0)
# Plot the new graph.
sns.set(style="whitegrid")
ax = sns.countplot(x="stars", data=restaurants_df, palette="Blues_d")
plt.title('Distribución de las estrellas')
plt.rcParams['figure.figsize'] = (20, 5)
plt.show()
Observamos como la variable objetivo tras la converssión está prácticamente balanceada.
En este apartado se realizará el estudio exploratorio de los datos pertenecientes al dataset restaurants_df. Para este análisis, nos centraremos en todo el dataset, de manera que en caso de realizar una transformación se aplique a todo el conjunto.
En el dataset restaurants_df las variables tienen un correcto nombramiento, permiten diferenciarse facilmente, por lo que se puede trabajar facilmente con ellas. Esto se debe principalmente al proceso de preprocesamiento realizado en el documento 00_Preprocessing donde diferenciamoes entre las variables atributos, categorías y subgrupos. Es por ello que no realizaremos ninguna modificación en cuanto a nombre de variables
Respecto a los aspectos éticos, todas las variables cumplen aspéctos éticos y no son sensibles, por lo que en este aspecto tampoco se eliminarán variables. Tal y como indica la Comisión Europea en este artículo
Sin embargo, existen variables que para la categoría de restaurantes nos van a ser irrelevantes, como los atributos relacionados con peluquería (attributes_HairSpecializesIn_*).
Además, se procede a eliminar las variables referentes a la localización puesto que la latitud y la longitud ya nos aporta información suficiente. Por ello se ha prescindido de las variables address y postal_code.
# Drop variables that are not relevant for the restarutants. Those ones that starts by 'attributes_HairSpecializesIn_'.
restaurants_df = restaurants_df.drop([col for col in restaurants_df.columns if col.startswith('attributes_HairSpecializesIn')], axis=1)
# Drop the variables of 'adress', 'postal_code'.
restaurants_df = restaurants_df.drop(['address', 'postal_code'], axis=1)
Comprobamos que se haya eliminado la variable observando la nueva estructura del dataframe restaurants_df:
# Print the current shape of 'restaurants_df'.
print("El DF restaurants_df contiene " + str(restaurants_df.shape[0]) +" filas y " + str(restaurants_df.shape[1]) + " columnas")
El DF restaurants_df contiene 34516 filas y 90 columnas
Realizamos el análisis de valores duplicados, comprobando si existe alguna fila duplicada. En caso positivo nos mostrará el nº.
# Save in 'restaurants_duplicated' the duplicated rows.
restaurants_duplicated = restaurants_df[restaurants_df.duplicated()]
# Print the number of duplicated rows in 'restaurants_duplicated'
print('Hay un número de '+str(len(restaurants_duplicated))+ " valores duplicados en el dataset restaurants_df")
Hay un número de 0 valores duplicados en el dataset restaurants_df
No hay valores duplicados, por lo que podemos continuar con el análisis
En este apartado procederemos a analizar el nº de valores nulos de cada variable por columnas y filas y entender su significado.
# Sum the number of nulls values of the different columns and rows, then the data is sorted from highest to lowest.
pd_series_null_columns = restaurants_df.isnull().sum().sort_values(ascending=False)
pd_series_null_rows = restaurants_df.isnull().sum(axis=1).sort_values(ascending=False)
# Create two dataframes showing the null values and proportion of business_df in rows and columns.
pd_null_columnas = pd.DataFrame(pd_series_null_columns, columns=['Valores nulos en columnas'])
pd_null_filas = pd.DataFrame(pd_series_null_rows, columns=['Valores nulos en filas'])
pd_null_columnas['porcentaje'] = pd_null_columnas['Valores nulos en columnas']/business_df.shape[0]*100
pd_null_filas['porcentaje_filas']= pd_null_filas['Valores nulos en filas']/business_df.shape[1]
Análizamos el número de valores nulos en filas y su proporción:
# Show the number of null values in the rows and the proportion.
(pd_null_filas[pd_null_filas['Valores nulos en filas']>0].value_counts()).sort_values(ascending=False)
Valores nulos en filas porcentaje_filas
35 0.35 2493
34 0.34 2163
36 0.36 1993
33 0.33 1432
37 0.37 1420
...
12 0.12 21
11 0.11 8
10 0.10 3
8 0.08 1
6 0.06 1
Length: 66, dtype: int64
Observamos como en el 6% de las filas hay al menos 6 variables con valores nulos. Aumentando gradualmente hasta el máximo de nulos por filas, siendo 35, con una proporción del 35%.
Mostramos las variables con valores nulos en columnas y proporción:
# Show the variables with more than 0 null values in columns.
pd_null_columnas[pd_null_columnas['Valores nulos en columnas']>0]
| Valores nulos en columnas | porcentaje | |
|---|---|---|
| attributes_AcceptsInsurance | 34501 | 26.150989 |
| attributes_RestaurantsCounterService | 34500 | 26.150231 |
| attributes_DietaryRestrictions_dairy-free | 34498 | 26.148715 |
| attributes_DietaryRestrictions_kosher | 34498 | 26.148715 |
| attributes_DietaryRestrictions_vegan | 34498 | 26.148715 |
| ... | ... | ... |
| total_hours_Saturday | 4041 | 3.062988 |
| total_hours_Wednesday | 4023 | 3.049344 |
| total_hours_Thursday | 3550 | 2.690821 |
| total_hours_Friday | 3385 | 2.565755 |
| attributes_RestaurantsTakeOut | 2854 | 2.163268 |
73 rows × 2 columns
Observamos como en las columnas de los atributos puede llegar a haber hasta un 26% de valores nulos. Principalmente se puede deber a que el restaurante no ha indicado si dispone o no de ese atributo, por lo que a la hora de la realización de los modelos decidiremos que imputación realizar a estos valores.
Las variables categóricas han de ser tratadas posteriormente para introducirlas en un modelo de Machine Learning. Para ello, se procederá a realizar un análisis de cada una de ellas para ver si es necesario realizar algún tipo de transformación y determinar como se comportan frente a la variable objetivo.
Crearemos una lista con las variables categóricas, con para mostrar sus diferentes valores.
# Create a DF with all the categorical variables.
categorical_variables = restaurants_df[[col for col in restaurants_df.columns if not col.startswith('total_') and not col.startswith('review_count')]]
Mostramos los diferentes valores existentes para cada variable y número de distintos.
# Create a DF with categorical_variables as rows and the number of unique values and distinct values as columns.
categorical_variables_df = pd.DataFrame(columns=['unique_values', 'distinct_values'])
for i in categorical_variables:
categorical_variables_df.loc[i, 'unique_values'] = restaurants_df[i].nunique()
categorical_variables_df.loc[i, 'distinct_values'] = restaurants_df[i].unique().tolist()
# Display the first 5 rows of the categorical_variables_df.
categorical_variables_df
| unique_values | distinct_values | |
|---|---|---|
| business_id | 34516 | [MTSW4McQd7CbVtyjqoe9mw, CF33F8-E6oudUQ46Hnavj... |
| name | 23102 | [St Honore Pastries, Sonic Drive-In, Vietnames... |
| city | 839 | [Philadelphia, Ashland City, Nashville, Tampa ... |
| state | 16 | [PA, TN, FL, IN, NV, IL, NJ, CA, LA, DE, MO, A... |
| latitude | 33564 | [39.9555052, 36.269593, 36.2081024, 27.9552692... |
| ... | ... | ... |
| attributes_DietaryRestrictions_vegan | 2 | [nan, 1.0, 0.0] |
| attributes_DietaryRestrictions_kosher | 1 | [nan, 0.0] |
| attributes_DietaryRestrictions_halal | 2 | [nan, 0.0, 1.0] |
| attributes_DietaryRestrictions_soy-free | 2 | [nan, 1.0, 0.0] |
| attributes_DietaryRestrictions_vegetarian | 2 | [nan, 1.0, 0.0] |
81 rows × 2 columns
Como es normal los valores relacionados con los nombres, ID, coordenadas... son númerosos debido a la cantidad de ubicaciones y restaurantes diferentes existentes en el dataset.
# Group by type of operation the number of stars.
df_vegan = pd.DataFrame(restaurants_df.groupby(('attributes_DietaryRestrictions_vegan'),
as_index=False)['stars'].sum())
# Plot an histogram of 'df_vegan'
fig = px.histogram(df_vegan, x='attributes_DietaryRestrictions_vegan', y='stars', color= 'attributes_DietaryRestrictions_vegan')
fig.show()
Gráficamente se puede observar que la mayoría de los restaurantes que no tiene restricciones para una dieta vegana obtienen mayor valoración, por lo que sería interesante que los restaurantes sean versátiles con este tipo de comensal.
# Group by type of operation the number of stars.
df_vegan = pd.DataFrame(restaurants_df.groupby(('attributes_DietaryRestrictions_vegetarian'),
as_index=False)['stars'].sum())
# Plot an histogram of 'df_vegan'
fig = px.histogram(df_vegan, x='attributes_DietaryRestrictions_vegetarian', y='stars', color= 'attributes_DietaryRestrictions_vegetarian')
fig.show()
La proporción es prácticamente similar al de los veganos puesto que son problemáticas similares. Los restaurantes que no cuentan con restricciones para proporcionar servicio a vegetarianos obtienen mayor valoración.
# Group by type of operation the number of stars.
df_gluten = pd.DataFrame(restaurants_df.groupby(('attributes_DietaryRestrictions_gluten-free'),
as_index=False)['stars'].sum())
# Plot an histogram of 'df_vegan'
fig = px.histogram(df_gluten, x='attributes_DietaryRestrictions_gluten-free', y='stars', color= 'attributes_DietaryRestrictions_gluten-free')
fig.show()
La Celiaquía es un problema presente en muchas personas, por lo que es importante saber si el restaurante tiene restricciones para este tipo de comensal, considerando esta variable en nuestra opinión una variable de vital importancia a la hora de que las estrellas de valoración sean altas. En este caso sorprende los que se desprende del gráfico puesto que no le dan importancia a esta variable. Si es cierto que no hay muchas personas con este tipo de problemas y es por lo que probablemente no se le de importancia, pero es una variable que se debería tener en cuenta para ofrecer un mejor servicio.
# Group by type of operation the number of stars.
df_delivery = pd.DataFrame(restaurants_df.groupby(('attributes_RestaurantsDelivery'),
as_index=False)['stars'].sum())
# Plot an histogram of 'df_delivery'
fig = px.histogram(df_delivery, x='attributes_RestaurantsDelivery', y='stars', color= 'attributes_RestaurantsDelivery')
fig.show()
El servicio de reparto cada vez se está extendiendo más siendo una variable importante a la hora de que los clientes valoren mejor el restaurante. Si el restaurante tiene servicio de reparto, muchos de los clientes lo valorarán más puesto que no será necesario desplazarse al local para recoger el pedido. En este caso se observa como la mayoría de los restaurantes que ofrecen este tipo de servicio tienen una valoración más alta. Esto puede deberse también a que se llega a un mayor número de clientes puesto que no es necesario desplazarse al local.
# Group by the number of stars the variables that starts by 'attributes_BusinessParking'.
business_parking = ['attributes_BusinessParkinggarage', 'attributes_BusinessParkinglot', 'attributes_BusinessParkingstreet',
'attributes_BusinessParkingvalidated', 'attributes_BusinessParkingvalet']
df_variables = pd.DataFrame(restaurants_df.groupby((business_parking),
as_index=False)['stars'].sum())
# Plot 2 histograms by row of 'df_diets':
for variable in business_parking:
fig = px.histogram(df_variables, x=variable, y='stars', color= variable)
fig.show()
Un aspecto importante a tener en cuenta es la posibilidad de que si un comensal acude en transporte privado no tenga problemas para poder aparcar y dejar su vehículo. Esto facilita la conexión del restaurante, por ello se observa como la mayoría de los restaurantes que no cuentan con restricciones para aparcar tienen una valoración más alta.
# Group by type of operation the number of stars.
df_delivery = pd.DataFrame(restaurants_df.groupby(('attributes_BusinessAcceptsCreditCards'),
as_index=False)['stars'].sum())
# Plot an histogram of 'df_delivery'
fig = px.histogram(df_delivery, x='attributes_BusinessAcceptsCreditCards', y='stars', color= 'attributes_BusinessAcceptsCreditCards')
fig.show()
Últimamente, cada vez más personas utilizan la tarjeta para pagar, por lo que es interesante valorar si las personas valoran este atributo para dar una mayor puntuación al restaurante. En este caso el gráfico nos muestra lo que ya se esperaba, y es que aquellos restaurantes que aceptan tarjeta de crédito tienen mayor número de estrellas en su valoración.
# Convert latitude and longitude to float64.
restaurants_df['latitude'] = restaurants_df['latitude'].astype('float64')
restaurants_df['longitude'] = restaurants_df['longitude'].astype('float64')
# Select the geographical coordinates and the name of the restaurants.
restaurants = restaurants_df[['business_id','latitude','longitude','name','stars', 'state']]
# Set the mapbox access token.
mapbox_access_token = 'pk.eyJ1IjoiYWxtYXNzbml5YW1hdCIsImEiOiJjamQ3NGY2Zms0emhmMnFuMjJ5OGNvOWoxIn0.DKGZcDRHVFRYNiQBe1D_zw'
# Create scatterplot in the map.
fig = px.scatter_mapbox(restaurants,
lat='latitude',
lon='longitude',
hover_name='name',
hover_data=['stars', 'state'],
zoom=3,
height=900,
width=900,
color='stars',
color_continuous_scale=px.colors.sequential.Bluered,
labels={'stars':'Stars'},
)
fig.update_coloraxes(colorbar=dict(title='Stars', tickvals=[0,1], ticktext=['0','1']))
fig.update_layout(mapbox_style="open-street-map",
margin={"r":0,"t":0,"l":0,"b":0}
)
# Display the map.
fig.show()
Observando el mapa podemos apreciar dónde se encuentran los restaurantes que van a ser objeto de este estudio. Principalmente en diferentes estados de Estados Unidos, sin embargo en nuestro dataset también se incluyen restaurantes de Canada, únicamente del estado de Alberta. Eliminamos por ello los restaurantes pertenecientes a Canada
# Drop the rows with the state 'AB'.
restaurants_df = restaurants_df[restaurants_df['state'] != 'AB']
Analizamos el nº de estrellas por estado.
# Group by the state of the restaurants and show in one column the total of stars and in other column the total of restaurants.
df_stars = pd.DataFrame(restaurants_df.groupby(('state'),
as_index=False)['stars'].sum()).sort_values(by='stars', ascending=False).reset_index(drop=True)
df_stars['total_restaurants'] = restaurants_df.groupby(('state'),
as_index=False)['stars'].count()['stars']
# Display the first 5 rows of the df_stars.
df_stars.head(3)
| state | stars | total_restaurants | |
|---|---|---|---|
| 0 | PA | 3730 | 1775 |
| 1 | FL | 2945 | 660 |
| 2 | TN | 1385 | 692 |
Procedemos a representarlo en un mapa mediante una escala de verdes.
# Import the json file.
geo_data = '../data/maps/us-states.json'
# Create a map object.
map = folium.Map(location=[37.0902, -95.7129], zoom_start=4)
# Create a choropleth map.
folium.Choropleth(
geo_data=geo_data,
name='choropleth',
data=df_stars,
columns=['state', 'stars'],
key_on='feature.id',
fill_color='YlGn',
fill_opacity=0.7,
line_opacity=0.2,
legend_name='Total of stars'
).add_to(map)
# Plot the map.
map
Observamos que los estados con mayor nº de estrellas son de mayor a menor: Pensilvania, Florida, Tennessee, Indiana y Louisiana. Para representarlo en el mapa hemos utilizado una escala de verdes, sin embargo, al no haber restaurante en todos los estados, aquellos que son grises indican que no hay dato existente.
Tras realizar un análisis de variables categóricas se pueden especificar atributos que se consideran más importantes a la hora de que un restaurante tenga un mayor número de estrellas. Estos atributos son:
1. No tener restricciones para una dieta vegana.
2. No tener restricciones para una dieta vegetariana.
3. Tener restricciones para personas celíacas no afecta negativamente a la valoración
4. Tener servicio de reparto.
5. Tener facilidades para aparcar en el caso de que el cliente acuda con un vehículo particular.
6. Contar con el pago con tarjeta como medio de pago.
7. El mayor número de restaurantes se encuentra en Estados Unidos, y el mayor nº de puntuaciones se encuentra en el estado de Pensilvania.
Procedemos a analizar la realación de las variables categóricas utilizando el coeficiente V de Crammer.
# Create a list with the rows that will be included in the new dataframe.
rows = []
# Loop through all pairs of variables.
for var1 in categorical_variables:
col = []
for var2 in categorical_variables:
# Calculate Cramer's V for the pair of variables.
observed = pd.crosstab(categorical_variables[var1], categorical_variables[var2])
if observed.size == 0:
col.append(None)
continue
cramers = cramers_V(categorical_variables[var1], categorical_variables[var2])
col.append(round(cramers,2))
rows.append(col)
# Creation of the Cramer's V matrix.
cramers_results = np.array(rows)
matrix = pd.DataFrame(cramers_results, columns = categorical_variables.columns, index =categorical_variables.columns)
# Convert as index the 0 column.
matrix = matrix.set_index(matrix.columns[0])
# Display the Cramer's V matrix with all columns and rows.
matrix
| business_id | name | city | state | latitude | longitude | stars | is_open | attributes_ByAppointmentOnly | attributes_BusinessAcceptsCreditCards | ... | attributes_BestNights_thursday | attributes_BestNights_sunday | attributes_BestNights_saturday | attributes_DietaryRestrictions_dairy-free | attributes_DietaryRestrictions_gluten-free | attributes_DietaryRestrictions_vegan | attributes_DietaryRestrictions_kosher | attributes_DietaryRestrictions_halal | attributes_DietaryRestrictions_soy-free | attributes_DietaryRestrictions_vegetarian | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| business_id | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN | 1.0 | 1.0 | ... | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | NaN | 1.0 | 1.0 | 1.0 |
| name | 1.0 | 1.0 | 0.68 | 0.78 | 0.97 | 0.95 | 0.88 | NaN | 0.98 | 0.94 | ... | 0.87 | 0.82 | 0.88 | 1.0 | 1.0 | 1.0 | NaN | 1.0 | 1.0 | 1.0 |
| city | 1.0 | 0.68 | 1.0 | 1.0 | 0.99 | 0.98 | 0.05 | NaN | 0.11 | 0.07 | ... | 0.12 | 0.1 | 0.12 | 1.0 | 1.0 | 1.0 | NaN | 1.0 | 1.0 | 1.0 |
| state | 1.0 | 0.78 | 1.0 | 1.0 | 1.0 | 1.0 | 0.01 | NaN | 0.01 | 0.02 | ... | 0.02 | 0.01 | 0.01 | 0.41 | 1.0 | 0.57 | NaN | 1.0 | 0.47 | 0.45 |
| latitude | 1.0 | 0.97 | 0.99 | 1.0 | 1.0 | 1.0 | 0.98 | NaN | 0.99 | 0.98 | ... | 1.0 | 0.99 | 1.0 | 1.0 | 1.0 | 1.0 | NaN | 1.0 | 1.0 | 1.0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| attributes_DietaryRestrictions_vegan | 1.0 | 1.0 | 1.0 | 0.57 | 1.0 | 1.0 | 0.0 | NaN | NaN | 0.0 | ... | NaN | NaN | NaN | 0.28 | 0.17 | 0.78 | NaN | 0.0 | 0.17 | 0.42 |
| attributes_DietaryRestrictions_kosher | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| attributes_DietaryRestrictions_halal | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 0.0 | NaN | NaN | 0.0 | ... | NaN | NaN | NaN | 0.0 | 0.05 | 0.0 | NaN | 0.22 | 0.0 | 0.0 |
| attributes_DietaryRestrictions_soy-free | 1.0 | 1.0 | 1.0 | 0.47 | 1.0 | 1.0 | 0.09 | NaN | NaN | 0.0 | ... | NaN | NaN | NaN | 0.43 | 0.0 | 0.17 | NaN | 0.0 | 0.64 | 0.17 |
| attributes_DietaryRestrictions_vegetarian | 1.0 | 1.0 | 1.0 | 0.45 | 1.0 | 1.0 | 0.0 | NaN | NaN | 0.0 | ... | NaN | NaN | NaN | 0.28 | 0.01 | 0.42 | NaN | 0.0 | 0.17 | 0.78 |
81 rows × 81 columns
Consideramos que las variables business_id, name, city y 'is_open' no tienen importancia a la hora de analizar la relación entre variables categóricas, por lo que no se eliminarán de la tabla resultante.
# Drop the 'business_id', 'name' and 'city' columns.
matrix = matrix.drop(columns=['business_id', 'name', 'city','is_open'])
# Drop the 'business_id', 'name' and 'city' rows.
matrix = matrix.drop(index=['business_id', 'name', 'city','is_open'])
# Display the Cramer's V matrix with all columns and rows.
matrix
Observamos como debido a la existencia de tantos valores nulos, algunas variables no obtienen correlación. Es por ello que la transformación de este tipo de datos, el cual se realizará en ´02_FeatureEngineering´ será crucial para la creación del modelo.
Como el tiempo de cómputo para calcular la matriz de V de Crammer ha sido muy elevado se ha decidido exportar la matriz en un archivo .csv, por si fuera necesario cargarlo en un futuro.
# Export to .csv.
matrix.to_csv("../data/processed/matrix_crammer.csv")
En este apartado procederemos a analizar las correlaciones existentes entre las diferentes variables numéricas. Para ello crearemos un dataframe con los valores numéricos.
# Select the numeric variables to save them in the new dataframe 'num_var_df'. Variables that start by 'total_' and 'review_count'.
num_var_df = restaurants_df[[col for col in restaurants_df.columns if col.startswith('total_') or col.startswith('review_count')]]
Para medir la correlación entre las diferentes variables numéricas realizaremos una matriz de correlación mediante el método Pearson.
# Display the correlation matrix of the 'num_var_df'.
get_corr_matrix(dataset = num_var_df, metodo='pearson', size_figure=[20,15])
Observamos como existe una fuerte correlación entre los horarios, principalmente porque son similiares durante los días de la semana. Además vemos como a cuanto mayor número de checkins de los usuarios mayor número de reviews que publican acerca del restaurante
En este apartado realizaremos un estudio de Outliers, es decir, detectar aquellos valores dentro de una muestra que no son consistentes con el resto.
# Obtain the outliers of the 'num_var_df' dataset.
outliers = get_outliers(num_var_df)
# Display number of rows withous NaN values.
print(outliers.count())
# Boxplot of the 'num_var_df' dataset.
fig = px.box(num_var_df, y=num_var_df.columns)
fig.show()
review_count 3368 total_hours_Monday 0 total_hours_Tuesday 1795 total_hours_Wednesday 1799 total_hours_Thursday 1836 total_hours_Friday 1682 total_hours_Saturday 1674 total_hours_Sunday 1783 total_checkins 3850 dtype: int64
Observamos como las variables con mayor nº de outliers es total_checkins y review_count. Esto se puede deber a que la fama de algún restaurante sea mayor y provoque un aumento de las reviews, aunque también pueden ser opiniones compradas. Por lo que a la hora de realizar los modelos estudiaremos como trabajar con estos datos.
Procedemos a exportar el dataset que hemos ido transformando durante el EDA, para posteriormente en el paso de ingeniería de variables, dividirlo en Train, Validación y Test:
# Export 'restaurants_df' as csv in the folder of processed data.
restaurants_df.to_parquet("../data/processed/restaurants_df.parquet")